﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;

namespace WSBackup
{

    public delegate bool OnProgress(string message);

    class FileArchiver : SevenZip.ICodeProgress, IDisposable
    {
        public const int buffer_length = 1024 * 16;

        private List<Revision> _ListOfRevisions;
        private OnProgress Progress;

        public FileArchiver(string archiveFileName, OnProgress progress)
        {
            this.Progress = progress;
            this.ArchiveFullFileNameIn = Path.GetFullPath(archiveFileName);
            this.DirectoryName = Path.GetDirectoryName(archiveFileName);
            this.FileName = Path.GetFileNameWithoutExtension(archiveFileName);

            Debug.WriteLineIf(false, "Create FileArchiver " + FileName);

            var backup_extension = Path.GetExtension(archiveFileName).ToLower();
            switch (backup_extension)
            {
                case ".bck":
                    CompresionMethod = CompresionMethod.Flat;
                    break;

                case ".bck7z":
                    CompresionMethod = CompresionMethod.SevenZip;
                    break;

                case ".bck7zip":
                    CompresionMethod = CompresionMethod.Undefined;
                    break;

                default:
                    throw new Exception("Wrong backup file extension. Only bck and bck7z is allowed.");
            }

            if (!Directory.Exists(DirectoryName))
            {
                Directory.CreateDirectory(DirectoryName);
            }

            //MS DataStream = new MemoryStream();
            InternalDataStream = new WSStream(@"C:\TEMP\WSBackup.tmp", archiveFileName);

            _ListOfRevisions = new List<Revision>();

            if (File.Exists(ArchiveFullFileNameIn))
            {
                using (FileStream fileStream = File.Open(ArchiveFullFileNameIn, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {

                    if (CompresionMethod == CompresionMethod.Undefined)
                    {
                        var cm = fileStream.ReadByte();
                        switch (cm)
                        {

                            case '7':
                                CompresionMethod = CompresionMethod.SevenZip;
                                break;

                            case 'F':
                                CompresionMethod = CompresionMethod.Flat;
                                break;

                            default:
                                throw new Exception("Neznamy format");
                        }
                    }

                    if (CompresionMethod == CompresionMethod.SevenZip)
                    {
                        Decode7z(InternalDataStream, fileStream);
                    }
                    else
                    {
                        InternalDataStream.CopyFrom(fileStream, progress);
                    }
                }

                var offset = 0U;
                var buff = new byte[8];
                while (offset + 8 <= InternalDataStream.Length)
                {
                    InternalDataStream.Position = offset;
                    InternalDataStream.Read(buff, 0, 8);
                    var val = BitConverter.ToUInt64(buff, 0);
                    var fh = new RevisionHeader(val);
                    _ListOfRevisions.Add(new Revision(fh, offset));
                    offset += fh.Size + 8;
                }

            }

        }

        public RevisionHeader[] Revisions
        {
            get { return _ListOfRevisions.Select(r => new RevisionHeader(r.Size, r.DateTime)).ToArray(); }
        }

        private string ArchiveFullFileNameIn { get; set; }

        private string ArchiveFullFileNameOut
        {
            get { return this.DirectoryName + "\\" + this.FileName + ".bck7zip"; }
        }

        private string DirectoryName { get; set; }

        private string FileName { get; set; }

        private WSStream InternalDataStream { get; set; }

        public CompresionMethod CompresionMethod { get; private set; }

        public bool AddRevision(string srcFilePath)
        {
            var srcFileName = Path.Combine(srcFilePath, this.FileName);
            if (!File.Exists(srcFileName))
                throw new Exception("Súbor " + srcFileName + " nenájdený");

            var date = File.GetLastWriteTime(srcFileName);
            using (var data = new FileStream(srcFileName, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                return AddRevision(date, data);
            }

        }

        private bool AddRevision(DateTime date, FileStream data)
        {
            //Revizia sa pridava az na koniec a tak dlzka je zaroven offset novej revizie
            var offset = (UInt32)InternalDataStream.Length;

            //Zistim pocet revizii
            var revision_count = Revisions.Length;
            if (revision_count > 0)
            {
                //vytiahnutie poslednej revizie
                using (var last_revision = GetRevision(revision_count - 1, @"C:\Temp\WSBackupRev.tmp"))
                {
                    //Porovnanie, ci posledna verzia a aktualna nie su zhodne
                    if (Util.CompareStream(data, last_revision, Progress))
                        return false;
                }
            }

            var header = new RevisionHeader((UInt32)data.Length, date);
            header.AppendHeader(InternalDataStream);

            //InternalDataStream.Write(data, 0, data.Length);
            var buffer = new byte[buffer_length];
            while (data.Position < data.Length)
            {
                var size = data.Read(buffer, 0, buffer_length);
                InternalDataStream.Write(buffer, 0, size);
            }

            _ListOfRevisions.Add(new Revision(header, offset));

            using (FileStream fs = new FileStream(ArchiveFullFileNameOut, FileMode.Create))
            {
                var use7zip = this.CompressionTest();
                fs.WriteByte((byte)(use7zip ? '7' : 'F'));
                if (use7zip)
                {
                    //File.WriteAllBytes(ArchiveFullFileName, Encode7z(DataStream));
                    Encode7z(fs, InternalDataStream);
                }
                else
                {
                    //File.WriteAllBytes(ArchiveFullFileName, Data);
                    InternalDataStream.WriteTo(fs);
                }
                fs.Flush();
                fs.Close();
            }

            return true;
        }

        public void Upgrade()
        {

            var lastWriteTime = File.GetLastWriteTime(this.ArchiveFullFileNameIn);

            using (FileStream fs = new FileStream(ArchiveFullFileNameOut, FileMode.Create))
            {
                var use7zip = this.CompressionTest();
                fs.WriteByte((byte)(use7zip ? '7' : 'F'));
                if (use7zip)
                {
                    //File.WriteAllBytes(ArchiveFullFileName, Encode7z(DataStream));
                    Encode7z(fs, InternalDataStream);
                }
                else
                {
                    //File.WriteAllBytes(ArchiveFullFileName, Data);
                    InternalDataStream.WriteTo(fs);
                }
                fs.Flush();
                fs.Close();
            }

            File.SetLastWriteTime(this.ArchiveFullFileNameOut, lastWriteTime);
            File.Delete(this.ArchiveFullFileNameIn);

        }

        private void Encode7z(Stream encodingStream, Stream inputStream)
        {
            inputStream.Seek(0, SeekOrigin.Begin);
            SevenZip.LZMA.Encoder encoder = new SevenZip.LZMA.Encoder();
            //encoder.SetCoderProperties(propIDs, properties);
            //encoder.WriteCoderProperties(encodingStream);
            PutInt32(encodingStream, (Int32)inputStream.Length);
            encoder.Code(inputStream, encodingStream, inputStream.Length, -1, this);
        }

        private void Decode7z(Stream decodingStream, Stream inputStream)
        {
            decodingStream.Seek(0, SeekOrigin.Begin);
            SevenZip.LZMA.Decoder decoder = new SevenZip.LZMA.Decoder();
            var prop = new byte[] { 93, 0, 0, 128, 0 };
            decoder.SetDecoderProperties(prop);
            Int64 outSize = GetInt32(inputStream);
            Int64 compressedSize = inputStream.Length - inputStream.Position;
            decoder.Code(inputStream, decodingStream, compressedSize, outSize, this);
        }

        private bool CompressionTest()
        {
            using (var input_ms = new MemoryStream())
            using (var output_ms = new MemoryStream())
            {
                var buffer = new byte[4096];
                InternalDataStream.Seek(0, SeekOrigin.Begin);
                var bytes = InternalDataStream.Read(buffer, 0, buffer.Length);
                input_ms.Write(buffer, 0, bytes);
                Encode7z(output_ms, input_ms);
                var ratio = (float)output_ms.Length / input_ms.Length;
                System.Diagnostics.Debug.WriteLine(string.Format("{0} {1} {2:P2}", input_ms.Length, output_ms.Length, ratio));
                return ratio < 0.8;
            }
        }

        public FileStream GetRevision(int index, string filename)
        {
            if (index == -1)
                index = _ListOfRevisions.Count - 1;
            else if (index < -1 || index >= _ListOfRevisions.Count)
                throw new Exception("Revision not exist");
            var rev = _ListOfRevisions[index];
            var revision = new FileStream(filename, FileMode.Create);
            InternalDataStream.Seek(rev.Offset, SeekOrigin.Begin);
            //InternalDataStream.Read(buffer, 0, rev.Size);

            var last = DateTime.Now.AddMilliseconds(100);
            var total = rev.Size;
            var needbytes = total;
            var buffer = new byte[buffer_length];
            while (needbytes > 0)
            {
                var read = (int)Math.Min(buffer_length, needbytes);
                var bytes = InternalDataStream.Read(buffer, 0, read);
                if (bytes != read)
                    throw new Exception("A toto ba byt co?");
                revision.Write(buffer, 0, bytes);
                needbytes -= (uint)bytes;
                if (Progress != null && DateTime.Now > last)
                {
                    Progress(string.Format("Retrieving Last Version {0:P2}", 1.0 - (double)needbytes / total));
                    last = DateTime.Now.AddMilliseconds(250);
                }
            }

            revision.Position = 0;
            return revision;
        }

        public void GetRevisionToFile(int index, string filename)
        {
            if (index == -1)
                index = _ListOfRevisions.Count - 1;
            else if (index < -1 || index >= _ListOfRevisions.Count)
                throw new Exception("Revision doesn't exist");

            var rev = _ListOfRevisions[index];
            using (var fsRevision = new FileStream(filename, FileMode.Create))
            {
                InternalDataStream.Seek(rev.Offset, SeekOrigin.Begin);
                //InternalDataStream.Read(buffer, 0, rev.Size);

                var last = DateTime.Now.AddMilliseconds(100);
                var total = rev.Size;
                var needbytes = total;
                var buffer = new byte[buffer_length];
                while (needbytes > 0)
                {
                    var read = (int)Math.Min(buffer_length, needbytes);
                    var bytes = InternalDataStream.Read(buffer, 0, read);
                    if (bytes != read)
                        throw new Exception("A toto ba byt co?");
                    fsRevision.Write(buffer, 0, bytes);
                    needbytes -= (uint)bytes;
                    if (Progress != null && DateTime.Now > last)
                    {
                        Progress(string.Format("Retrieving Last Version {0:P2}", 1.0 - (double)needbytes / total));
                        last = DateTime.Now.AddMilliseconds(250);
                    }
                }

                fsRevision.Flush();
                fsRevision.Close();
            }

        }

        private long GetInt64(Stream stream)
        {
            var buff = new byte[8];
            stream.Read(buff, 0, 8);
            return BitConverter.ToInt64(buff, 0);
        }

        private long GetInt32(Stream stream)
        {
            var buff = new byte[4];
            stream.Read(buff, 0, 4);
            return BitConverter.ToInt32(buff, 0);
        }

        private void PutInt64(Stream stream, Int64 value)
        {
            stream.Write(BitConverter.GetBytes(value), 0, 8);
        }

        private void PutInt32(Stream stream, Int32 value)
        {
            stream.Write(BitConverter.GetBytes(value), 0, 4);
        }

        public bool SetProgress(string message)
        {
            if (this.Progress == null)
                return true;

            return Progress(message);
        }

        public void SetLastWriteTime(DateTime lastWriteTime)
        {
            File.SetLastWriteTime(this.ArchiveFullFileNameIn, lastWriteTime);
        }

        // Flag: Has Dispose already been called? 
        bool m_disposed = false;

        // Public implementation of Dispose pattern callable by consumers. 
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        // Protected implementation of Dispose pattern. 
        protected virtual void Dispose(bool disposing)
        {

            if (m_disposed)
                return;

            if (disposing)
            {

                if (InternalDataStream != null)
                {
                    InternalDataStream.Dispose();
                    InternalDataStream = null;
                }

                // Free any unmanaged objects here. 
                m_disposed = true;

                Debug.WriteLineIf(false, "Dispose FileArchiver " + FileName);

            }

        }
    }
}
